﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Win32;

namespace OrbClientCacher
{
	public class OrbClientCacher
	{
		#region Fields
		private CrmClientParams parameters;

		private OrbCCSettings settings;

		private RegistryKey appKey;

		private RegistryKey crmKey;

		private System.Timers.Timer hostTimer;

		private System.Timers.Timer cachingTimer;

		private ManualResetEvent[] _resetEvents;

		private bool HostWasDown;

		private IList<IObserveCacher> observer;
		
		private bool IsCacherRunning;
		#endregion

		#region Properties
		private bool IsHostRunning
		{
			get
			{
				Process[] procs = Process.GetProcessesByName("Microsoft.Crm.Application.Hoster");

				if (procs.Length > 0)
					return true;

				return false;
			}
		}

		private bool IsOffline
		{
			get
			{
				return Utilities.GetSetValue<bool>(this.crmKey, "RCOffline", false);
			}
		}
		#endregion

		/// <summary>
		/// Konstruktor
		/// </summary>
		/// <param name="observer">Observerreferenz</param>
		public OrbClientCacher()
		{

			this.observer = new List<IObserveCacher>();
			
			this.HostWasDown = true;

			this.ReadSettings();
			this.ReadCrmConfig();

			this.InitLogger();
		}

		#region Konfiguration
		/// <summary>
		/// Lesen der Anwendungskonfiguration
		/// </summary>
		private void ReadSettings()
		{
			// Standardeinstellungen und App-RegistryKey laden
			this.settings = OrbCCSettings.DefaultCrmClientCacherSettings();
			this.appKey = Registry.CurrentUser.CreateSubKey(@"SOFTWARE\ORBIS AG\OrbClientCache");

			// Webservice-Liste auslesen
			this.settings.AsmxIndex = Utilities.GetSetJSON<IteratingList<string>>("asmxlist.json", this.settings.AsmxIndex);

			// Blacklist auslesen
			this.settings.BlackList = Utilities.GetSetJSON<IList<string>>("blacklist.json", this.settings.BlackList);

			// CacheingTimer aus Registry lesen
			this.settings.CachingTimer = Utilities.GetSetValue<int>(this.appKey, "CachingTimer", this.settings.CachingTimer, true);

			// Etc-Codes auslesen
			this.settings.EtcList = Utilities.GetSetJSON<IList<int>>("etclist.json", this.settings.EtcList);

			// Timer für den Host-Alive-Check laden
			this.settings.HostCheckTimer = Utilities.GetSetValue<int>(this.appKey, "HostCheckTimer", this.settings.HostCheckTimer, true);

			// Initiale Wartezeit für den Cacher
			this.settings.InitialCacheWaitTime = Utilities.GetSetValue<int>(this.appKey, "InitialCacheWaitTime", this.settings.InitialCacheWaitTime, true);

			// SmartCache Flag auslesen
			this.settings.SmartCache = Utilities.GetSetValue<bool>(this.appKey, "SmartCache", this.settings.SmartCache, true);

			// Thread Count laden
			this.settings.Threads = Utilities.GetSetValue<int>(this.appKey, "Threads", this.settings.Threads, true);

			// Throttle laden (Zeit in Millisekunden zwischen den Anfragen)
			this.settings.Throttle = Utilities.GetSetValue<int>(this.appKey, "Throttle", this.settings.Throttle, true);

			// Sub-Path laden für ASPx Caching
			this.settings.WebSubPath = Utilities.GetSetValue<string>(this.appKey, "WebSubPath", this.settings.WebSubPath, true);

			// Toplist auslesen
			this.settings.TopList = Utilities.GetSetJSON<IList<Site>>("toplists.json", this.settings.TopList);

			// Trace-Flag auslesen
			this.settings.Trace = Utilities.GetSetValue<bool>(this.appKey, "Trace", this.settings.Trace, true);

			// Trace-Path auslesen
			this.settings.TracePath = Utilities.GetSetValue<string>(this.appKey, "TracePath", this.settings.TracePath, true);

		}

		/// <summary>
		/// Lesen der CRM-Client Konfiguration
		/// </summary>
		private void ReadCrmConfig()
		{

			this.parameters.ClientInstalled = true;

			// CRM Parameter laden
			this.crmKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\MSCRMClient", false);

			if (this.crmKey == null)
			{
				this.WriteError("Missing MSCRM installation or at least Registry-Key. MSCRM Client is not properly installed.");
				this.parameters.ClientInstalled = false;
				return;
			}

			// Installationspfad aus der Registry lesen
			this.parameters.InstallPath = Utilities.GetSetValue<string>(this.crmKey, "InstallPath", null);
			if (this.parameters.InstallPath == null)
			{
				this.WriteError("Missing value for \"InstallPath\" value on MSCRMClient Key. MSCRM Client is not properly installed.");
				this.parameters.ClientInstalled = false;
			}

			this.parameters.CassiniPort = Utilities.GetSetValue<int>(this.crmKey, "CassiniPort", -1);
			if (this.parameters.CassiniPort == -1)
			{
				this.WriteError("Missing value for \"CassiniPort\" value on MSCRMClient Key. MSCRM Client is not properly installed.");
				this.parameters.ClientInstalled = false;
			}

			this.parameters.ClientAuthOrganizationName = Utilities.GetSetValue<string>(this.crmKey, "ClientAuthOrganizationName", null);
			if (this.parameters.ClientAuthOrganizationName == null)
			{
				this.WriteError("Missing value for \"ClientAuthOrganizationName\" value on MSCRMClient Key. MSCRM Client is not properly installed.");
				this.parameters.ClientInstalled = false;
			}

			string versionString = Utilities.GetSetValue<string>(this.crmKey, "CRM_Client_Version", null);
			if (versionString == null)
			{
				this.WriteError("Missing value for \"ClientAuthOrganizationName\" value on MSCRMClient Key. MSCRM Client is not properly installed.");
				this.parameters.ClientInstalled = false;
			}
			else
			{
				this.parameters.CrmClientVersion = int.Parse(versionString.Split('.')[0]);
			}
		}
		#endregion

		#region ExternalInterface
		
		/// <summary>
		/// Startet den Cacher
		/// </summary>
		internal void Start()
		{

			// Abbrechen, wenn Client nicht installiert
			if (!this.parameters.ClientInstalled)
				return;

			this.IndexClientAspx(Path.Combine(this.parameters.InstallPath, this.settings.WebSubPath));
			this.NotifyIndexSize(this.settings.AspxIndex.Count + this.settings.AsmxIndex.Count);

			// Timer starten
			this.StartHostCheckTimer();
			this.StartCachingTimer();

			// Initiale Ausführung
			this.OnHostCheckTimerElapsed(null, null);
			Thread.Sleep(TimeSpan.FromSeconds(this.settings.InitialCacheWaitTime));
			this.OnCachingTimerElapsed(null, null);

		}

		/// <summary>
		/// Beendet den Cacher
		/// </summary>
		internal void Stop()
		{
			this.hostTimer.Close();
			this.cachingTimer.Close();
			this.WriteInfo("Stopping OrbClientCacher...");
		}

		/// <summary>
		/// Startet den Cache erneut
		/// </summary>
		internal void ReCache()
		{
			if (this.IsHostRunning && !this.IsCacherRunning)
			{
				this.StartCachingMechanism();
				this.HostWasDown = false;
			}
		}

		/// <summary>
		/// Fügt einen Observer der Liste hinzu
		/// </summary>
		/// <param name="observer">Der Observer</param>
		internal void AddObserver(IObserveCacher observer)
		{
			if (this.observer == null) return;
			this.observer.Add(observer);
		}
		#endregion

		#region AspxAsmxCaching
		
		/// <summary>
		/// Initialisiert den Timer, der den Cache überprüft
		/// </summary>
		private void StartCachingTimer()
		{
			this.cachingTimer = new System.Timers.Timer(this.settings.CachingTimer * 1000);
			this.cachingTimer.Elapsed += new System.Timers.ElapsedEventHandler(OnCachingTimerElapsed);
			this.cachingTimer.Enabled = true;
			this.cachingTimer.Start();
			GC.KeepAlive(this.cachingTimer);
		}

		/// <summary>
		/// Startet den Caching-Vorgang, wenn der Crm-Client Offline ist, der Host-Service beendet war, der
		/// Host läuft und derzeit kein Caching-Vorgang läuft.
		/// </summary>
		/// <param name="source"></param>
		/// <param name="e"></param>
		private void OnCachingTimerElapsed(object source, System.Timers.ElapsedEventArgs e)
		{
			// Wenn der Host down war UND der Host jetzt läuft Caching anstoßen
			if (this.IsOffline && this.HostWasDown && this.IsHostRunning && !this.IsCacherRunning)
			{
				this.StartCachingMechanism();
				this.HostWasDown = false;
			}
		}

		/// <summary>
		/// Startet das Caching der ASPX- und ASMX-Dateien in zwei seperaten Threads, die wiederum
		/// einen gemeinsamen Threadpool nutzen um die Dateien abzufragen beim Host.
		/// </summary>
		private void StartCachingMechanism()
		{
			this.IsCacherRunning = true;
			this._progressCounter = 0;
			
			// Threadpool States initialisieren
			if (this.settings.Threads > 64) this.settings.Threads = 64;
			this._resetEvents = new ManualResetEvent[this.settings.Threads];
			for (int i = 0; i < this.settings.Threads; i++) this._resetEvents[i] = new ManualResetEvent(true);

			this.NotifyState("Caching...");

			// Caching-Threads starten
			Task aspx = Task.Factory.StartNew(new Action(this.StartAspxCachingMechanism));
			Task asmx = Task.Factory.StartNew(new Action(this.StartAsmxCachingMechanism));

			// Cache Fertigstellung abwarten
			aspx.Wait();
			asmx.Wait();

			this.IsCacherRunning = false;
			this.NotifyState("Idle.");
		}

		/// <summary>
		/// Startet das ASPX-Caching (Webseiten)
		/// </summary>
		private void StartAspxCachingMechanism()
		{
			this.settings.AspxIndex.ResetCounter();
			while (this.settings.AspxIndex.HasNext())
			{
				// Warten bis ein Thread frei wird und dessen index speichern und Status resetten
				int index = WaitHandle.WaitAny(this._resetEvents);
				this._resetEvents[index].Reset();

				// Nächste Seite laden
				Site site = this.settings.AspxIndex.GetNext();

				// Parameter zusammenbauen
				object[] param = new object[] { site, index };

				Thread.Sleep(TimeSpan.FromMilliseconds(this.settings.Throttle));
				
				// Thread abschicken
				ThreadPool.QueueUserWorkItem(new WaitCallback(this.DoCacheAspx), param);
			}
		}

		/// <summary>
		/// Startet das ASMX-Caching (Webservice)
		/// </summary>
		private void StartAsmxCachingMechanism()
		{
			this.settings.AsmxIndex.ResetCounter();
			while (this.settings.AsmxIndex.HasNext())
			{
				// Warten bis ein Thread frei wird und dessen index speichern und Status resetten
				int index = WaitHandle.WaitAny(this._resetEvents);
				this._resetEvents[index].Reset();

				// Nächste Webservice-Adresse laden
				string asmx = this.settings.AsmxIndex.GetNext();

				// Parameter zusammenbauen
				object[] param = new object[] { asmx, index };
				Console.WriteLine(asmx);

				Thread.Sleep(TimeSpan.FromMilliseconds(this.settings.Throttle));
				
				// Thread abschicken
				ThreadPool.QueueUserWorkItem(new WaitCallback(this.DoCacheAsmx), param);
			}
		}

		/// <summary>
		/// Ausführende Methode, die ASPX-Dateien in einem zugewiesenen Thread ausführt
		/// </summary>
		/// <param name="o">Objekt mit der ASPX-Url und dem Threadindex</param>
		private void DoCacheAspx(object o)
		{
			object[] param = (object[])o;

			Site site = (Site)param[0];
			int threadIndex = (int)param[1];

			if (site.DoEtc)
			{
				foreach (int etc in this.settings.EtcList)
					this.CacheUrl(site.Url + "?etc=" + etc);
			}
			else
			{
				this.CacheUrl(site.Url);
			}

			// Fortschrittsbenachrichtigung
			this.NotifyProgress();

			// Ende Signal setzen
			this._resetEvents[threadIndex].Set();
		}

		/// <summary>
		/// Ausführende Methode, die ASMX-Dateien in einem zugewiesenen Thread ausführt
		/// </summary>
		/// <param name="o">Objekt mit der ASMX-Url und dem Threadindex</param>
		private void DoCacheAsmx(object o)
		{
			object[] param = (object[])o;

			string asmx = (string)param[0];
			int threadIndex = (int)param[1];

			this.CacheUrl(asmx);

			// Fortschrittsbenachrichtigung
			this.NotifyProgress();

			// Ende Signal setzen
			this._resetEvents[threadIndex].Set();

		}

		/// <summary>
		/// Erzeugt einen HttpWebRequest an die übergebene Url
		/// </summary>
		/// <param name="url">Die abzufragende Url</param>
		private void CacheUrl(string url)
		{
			string uri = "http://localhost:" + this.parameters.CassiniPort + "/" + url;
			try
			{
				HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uri);

				req.AllowAutoRedirect = true;
				req.MaximumAutomaticRedirections = 3;

				HttpWebResponse res = (HttpWebResponse)req.GetResponse();
			}
			catch (Exception e)
			{
				this.WriteWarning("Caching \"" + url + "\" failed. Message: " + e.Message);
			}
		}
		#endregion

		#region HostKeepAlive
		/// <summary>
		/// Initialisiert einen Timer, welcher in Intervallen prüft, ob der Host gestartet ist
		/// </summary>
		private void StartHostCheckTimer()
		{
			this.hostTimer = new System.Timers.Timer(this.settings.HostCheckTimer * 1000);
			this.hostTimer.Elapsed += new System.Timers.ElapsedEventHandler(OnHostCheckTimerElapsed);
			this.hostTimer.Enabled = true;
			this.hostTimer.Start();
			GC.KeepAlive(this.hostTimer);
		}

		/// <summary>
		/// Nach ablauf des Timers wird hier geprüft, ob der CRM Client offline ist und ob
		/// der Host-Service gestartet ist. Falls offline und Host nicht gestartet, dann
		/// wird der Host ausgeführt.
		/// </summary>
		/// <param name="source"></param>
		/// <param name="e"></param>
		private void OnHostCheckTimerElapsed(object source, System.Timers.ElapsedEventArgs e)
		{
			// Wenn Host nicht läuft starten
			if (this.IsOffline && !this.IsHostRunning)
			{
				this.HostWasDown = true;
				this.StartHostProcess();
			}
		}

		/// <summary>
		/// Startet den Host-Service
		/// </summary>
		/// <returns>false, wenn starten des Host-Service fehlschlägt</returns>
		private bool StartHostProcess()
		{

			string hostPath = Path.Combine(this.parameters.InstallPath, this.settings.WebSubPath, @"bin\Microsoft.Crm.Application.Hoster.exe");
			Process p = Process.Start(new ProcessStartInfo(hostPath));

			if (!this.IsHostRunning)
			{
				this.WriteError("Starting Microsoft.Crm.Application.Hoster failed. Exit Code:" + p.ExitCode);
				return false;
			}

			return true;
		}
		#endregion

		#region IndexingAspx

		/// <summary>
		/// Indiziert alle ASPX-Seiten im Client-Verzeichnis dabei werden die Blacklist
		/// und die Top-List beachtet und es erfolgt einen Priorisierung nach der
		/// angegebenen Priorität in dem Site-Objekt. Sollte in der Registry das
		/// SmartCache-Flag aktiviert sein, dann wird nur die Top-Liste in den Index
		/// übertragen.
		/// </summary>
		/// <param name="sDir">Das ausgangsverzeichnis, von dem aus rekursiv gesucht wird</param>
		private void IndexClientAspx(string sDir)
		{

			this.settings.AspxIndex = new IteratingList<Site>(450);

			if (this.settings.SmartCache)
			{
				// Smart cachen und nur die vordefinierte Liste ablaufen
				this.settings.AspxIndex.AddRange(this.settings.TopList);
			}
			else
			{
				// Alles cachen und alle Aspx-Seiten indizieren
				this.IndexClientAspxRecursive(sDir, true);
			}

			// Index sortieren nach Priorität
			IList<Site> aspxList = (from site in this.settings.AspxIndex
									orderby site.Priority ascending
									select site).ToList<Site>();
			
			this.settings.AspxIndex.Clear();
			this.settings.AspxIndex.AddRange(aspxList);

			this.WriteInfo("Indexing complete. There are currently " + this.settings.AspxIndex.Count + " pages indexed.");
		}
		
		/// <summary>
		/// Die eigentliche Rekursionsfunktion, welche die Verzeichnisse indiziert
		/// </summary>
		/// <param name="sDir">Ausgangsverzeichnis, von welchem aus weiter rekursiv gesucht wird</param>
		/// <param name="isInitial">true, wenn das der erste Aufruf der Rekursion ist</param>
		/// <param name="filterPath">String, der den Teil des Pfades angibt, der vom Gesamtpfad entfernt wird</param>
		private void IndexClientAspxRecursive(string sDir, bool isInitial = false, string filterPath = "")
		{
			// Wächter bricht ab, wenn ungültige übergabe
			if (sDir == null) return;

			// Pfad speichern, der entfernt werden soll
			if (isInitial) filterPath = sDir;

			bool blacklisted = false;

			// Durchlaufen der Verzeichnisstruktur
			try
			{

				// Inhalte des Verzeichnisses durchlaufen
				foreach (string file in Directory.GetFiles(sDir, "*.aspx"))
					this.AddFileToIndex(file, filterPath);
	
				foreach (string dir in Directory.GetDirectories(sDir))
				{
					// Prüfen, ob Verzeichnis in der Blacklist
					blacklisted = false;
					
					foreach (string bl in this.settings.BlackList)
					{
						string cmpDir = dir.ToLower();
						string cmpBl = bl.ToLower();
						if (cmpDir.Contains(cmpBl)) blacklisted = true;
					}
					
					if (blacklisted) continue;

					// Rekursion
					this.IndexClientAspxRecursive(dir, false, filterPath);
				}
			}
			catch
			{
				// Ignoriere Zugriffsfehler
			}
		}

		/// <summary>
		/// Fügt dem ASPX-Index eine ASPX-Datei hinzu.
		/// </summary>
		/// <param name="file">Der komplette Pfad der ASPX-Datei</param>
		/// <param name="filterPath">String, der den Teil des Pfades angibt, der vom Gesamtpfad entfernt wird</param>
		private void AddFileToIndex(string file, string filterPath)
		{
			// String normalisieren
			file = file.Substring(filterPath.Length + 1, file.Length - filterPath.Length - 1)
						.ToLower()
						.Replace(@"\", "/");

			// Site-Objekt mit niedrigster Priorität erzeugen
			Site site = new Site()
			{
				DoEtc = false,
				Priority = 3,
				Url = file
			};

			// Prüfen, ob die Datei in der Top-Liste existiert, wenn ja, diesen Eintrag übernehmen
			bool contains = this.settings.TopList.Select(s => s.Url).Contains<string>(site.Url);
			if (contains)
			{
				site = (from s in this.settings.TopList
						where s.Url == file
						select s).ToList<Site>()[0];
			}

			// Dem Index hinzufügen
			this.settings.AspxIndex.Add(site);
		}
		#endregion

		#region ObserverHandling
		
		/// <summary>
		/// Benachrichtigt die Observer über die größe des Cache-Index
		/// </summary>
		/// <param name="size">Die Anzahl der Elemente im Cache-Index</param>
		private void NotifyIndexSize(int size)
		{
			if (this.observer == null || this.observer.Count <= 0) return;
			foreach (IObserveCacher oc in this.observer)
				oc.NotifyIndexSize(size);
		}

		private int _progressCounter;
		
		/// <summary>
		/// Inkrementiert den ProgressCounter und benachrichtigt die Observer
		/// über den Fortschritt des Cachings
		/// </summary>
		private void NotifyProgress()
		{
			if (this.observer == null || this.observer.Count <= 0) return;
			foreach (IObserveCacher oc in this.observer)
				oc.NotifyProgress(++this._progressCounter);
		}

		/// <summary>
		/// Übergibt den Observern eine Statusmeldung
		/// </summary>
		/// <param name="state">Die Statusmeldung</param>
		private void NotifyState(string state)
		{
			if (this.observer == null || this.observer.Count <= 0) return;
			foreach (IObserveCacher oc in this.observer)
				oc.NotifyState(state);
		}
		#endregion

		#region Logging

		/// <summary>
		/// Initialisiert den Logging-Mechanismus
		/// </summary>
		private void InitLogger()
		{
			Trace.Listeners.Add(new TextWriterTraceListener(this.settings.TracePath, "TraceLog"));
			Trace.AutoFlush = true;
			Trace.TraceInformation("Starting OrbClientCacher...");
			Trace.Flush();
		}
	
		/// <summary>
		/// Schreibt einen Error ins Logfile
		/// </summary>
		/// <param name="message">Inhalt des Fehlers</param>
		private void WriteError(string message)
		{
			if (!this.settings.Trace) return;
			Trace.TraceError(message);
		}

		/// <summary>
		/// Schreibt eine Warnung ins Logfile
		/// </summary>
		/// <param name="message">Inhalt des Fehlers</param>
		private void WriteWarning(string message)
		{
			if (!this.settings.Trace) return;
			Trace.TraceWarning(message);
		}
		
		/// <summary>
		/// Schreibt eine Information ins Logfile
		/// </summary>
		/// <param name="message">Inhalt des Fehlers</param>
		private void WriteInfo(string message)
		{
			if (!this.settings.Trace) return;
			Trace.TraceInformation(message);
		}
		#endregion
	}
}
